A Go web server comprises one function — a net/http HandlerFunc(ResponseWriter, *Request) — for each of your API’s endpoints. Our API has two endpoints: Produce for writing to the log and Consume for reading from the log. When building a JSON/HTTP Go server, each handler consists of three steps:
Unmarshal the request’s JSON body into a struct.
Run that endpoint’s logic with the request to obtain a result.
Marshal and write that result to the response.
If your handlers become much more complicated than this, then you should move the code out, move request and response handling into HTTP middleware, and move business logic further down the stack.
Let’s start by adding a function for users to create our HTTP server. Inside your server directory, create a file called http.go that contains the following code:
LetsGo/internal/server/http.go
package server
import (
"encoding/json"
"net/http"
"github.com/gorilla/mux"
)
func NewHTTPServer(addr string) *http.Server {
httpsrv := newHTTPServer()
r := mux.NewRouter()
r.HandleFunc("/", httpsrv.handleProduce).Methods("POST")
r.HandleFunc("/", httpsrv.handleConsume).Methods("GET")
return &http.Server{
Addr: addr,
Handler: r,
}
}
NewHTTPServer(addr string) takes in an address for the server to run on and returns an *http.Server. We create our server and use the popular gorilla/mux library to write nice, RESTful routes that match incoming requests to their respective handlers. An HTTP POST request to / matches the produce handler and appends the record to the log, and an HTTP GET request to / matches the consume handler and reads the record from the log. We wrap our server with a *net/http.Server so the user just needs to call ListenAndServe to listen for and handle incoming requests.
Next, we’ll define our server and the request and response structs by adding this snippet below NewHTTPServer:
LetsGo/internal/server/http.go
type httpServer struct {
Log *Log
}
func newHTTPServer() *httpServer {
return &httpServer{
Log: NewLog(),
}
}
type ProduceRequest struct {
Record Record `json:"record"`
}
type ProduceResponse struct {
Offset uint64 `json:"offset"`
}
type ConsumeRequest struct {
Offset uint64 `json:"offset"`
}
type ConsumeResponse struct {
Record Record `json:"record"`
}
We now have a server referencing a log for the server to defer to in its handlers. A produce request contains the record that the caller of our API wants appended to the log, and a produce response tells the caller what offset the log stored the records under. A consume request specifies which records the caller of our API wants to read and the consume response to send back those records to the caller. Not bad for just 28 lines of code, huh?
Next, we need to implement the server’s handlers. Add the following code below your types from the previous code snippet:
LetsGo/internal/server/http.go
func (s *httpServer) handleProduce(w http.ResponseWriter, r *http.Request) {
var req ProduceRequest
err := json.NewDecoder(r.Body).Decode(&req)
if err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
off, err := s.Log.Append(req.Record)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
res := ProduceResponse{Offset: off}
err = json.NewEncoder(w).Encode(res)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
}
The produce handler implements the three steps we discussed before: unmarshaling the request into a struct, using that struct to produce to the log and getting the offset that the log stored the record under, and marshaling and writing the result to the response. Our consume handler looks almost identical. Add the following snippet below your produce handler:
LetsGo/internal/server/http.go
func (s *httpServer) handleConsume(w http.ResponseWriter, r *http.Request) {
var req ConsumeRequest
err := json.NewDecoder(r.Body).Decode(&req)
if err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
record, err := s.Log.Read(req.Offset)
if err == ErrOffsetNotFound {
http.Error(w, err.Error(), http.StatusNotFound)
return
}
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
res := ConsumeResponse{Record: record}
err = json.NewEncoder(w).Encode(res)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
}
The consume handler is like the produce handler but calls Read(offset uint64) to get the record stored in the log. This handler contains more error checking so we can provide an accurate status code to the client if the server can’t handle the request, like if the client requested a record that doesn’t exist.
That’s all the code needed for our server. Now let’s write some code to turn your server library into a program we can execute.
Source: Medium
The Tech Platform
Comments