DZone
Thanks for visiting DZone today,
Edit Profile
  • Manage Email Subscriptions
  • How to Post to DZone
  • Article Submission Guidelines
Sign Out View Profile
  • Post an Article
  • Manage My Drafts
Over 2 million developers have joined DZone.
Log In / Join
Please enter at least three characters to search
Refcards Trend Reports
Events Video Library
Refcards
Trend Reports

Events

View Events Video Library

Zones

Culture and Methodologies Agile Career Development Methodologies Team Management
Data Engineering AI/ML Big Data Data Databases IoT
Software Design and Architecture Cloud Architecture Containers Integration Microservices Performance Security
Coding Frameworks Java JavaScript Languages Tools
Testing, Deployment, and Maintenance Deployment DevOps and CI/CD Maintenance Monitoring and Observability Testing, Tools, and Frameworks
Culture and Methodologies
Agile Career Development Methodologies Team Management
Data Engineering
AI/ML Big Data Data Databases IoT
Software Design and Architecture
Cloud Architecture Containers Integration Microservices Performance Security
Coding
Frameworks Java JavaScript Languages Tools
Testing, Deployment, and Maintenance
Deployment DevOps and CI/CD Maintenance Monitoring and Observability Testing, Tools, and Frameworks

Because the DevOps movement has redefined engineering responsibilities, SREs now have to become stewards of observability strategy.

Apache Cassandra combines the benefits of major NoSQL databases to support data management needs not covered by traditional RDBMS vendors.

The software you build is only as secure as the code that powers it. Learn how malicious code creeps into your software supply chain.

Generative AI has transformed nearly every industry. How can you leverage GenAI to improve your productivity and efficiency?

Related

  • Adding a Gas Station Map to a React and Go/Gin/Gorm Application
  • What Is API-First?
  • A Camunda Greenhouse: Part 3
  • Go 1.24+ Native FIPS Support for Easier Compliance

Trending

  • Understanding the Shift: Why Companies Are Migrating From MongoDB to Aerospike Database?
  • Software Delivery at Scale: Centralized Jenkins Pipeline for Optimal Efficiency
  • The Perfection Trap: Rethinking Parkinson's Law for Modern Engineering Teams
  • Advancing Robot Vision and Control
  1. DZone
  2. Coding
  3. Languages
  4. Generic HTTP Handlers

Generic HTTP Handlers

Explore how we leveraged generics in Go to create a library for HTTP handlers that allow us to write more efficient, type-safe, reusable, and readable code.

By 
Simon Cheng user avatar
Simon Cheng
·
Jan. 23, 24 · Tutorial
Likes (3)
Comment
Save
Tweet
Share
1.6K Views

Join the DZone community and get the full member experience.

Join For Free

The Go programming language is the primary language used by Vaunt. Our developers typically gravitate towards Golang for its simplicity, performance, joyful syntax, and standard library capabilities. In March of 2022, Go added its support for generics, a highly anticipated and often controversial feature that has been long-awaited by the community. Generics enable developers to create functions, methods, and data structures that can operate on any type rather than being limited to a specific type. 

In this blog post, we'll explore how we leveraged generics in Go to create a library for HTTP handlers. Specifically, we'll look at how we created a framework that can handle requests for any data type, allowing us to write more efficient, type-safe, reusable, and readable code.

Let's delve into the usage of generics in HTTP handlers by utilizing our generic, lightweight handler framework called GLHF.

Common Marshalling Patterns

Go's standard library has excellent support for building REST APIs. However, there are often common patterns that nearly all HTTP handlers have to implement, such as marshaling and unmarshaling requests and responses. This creates areas of code redundancy, which can lead to errors.

Let's take a look at a simple HTTP server that implements a few routes using the standard HTTP library and Gorilla Mux for basic URL parameter parsing.

Warning: Gorilla Mux has been archived. We still leverage Gorilla Mux within Vaunt, but we recommend looking at alternatives, such as HTTPRouter.

Below is a simple example HTTP API that implements creating and retrieving a todo.

HTTP
 
package main

import (
    "context"
    "encoding/json"
    "fmt"
    "log"
    "net/http"
    "os"
    "os/signal"
    "time"

    "github.com/VauntDev/glhf/example/pb"
    "github.com/gorilla/mux"
    "golang.org/x/sync/errgroup"
    "golang.org/x/sys/unix"
)

type TodoService struct {
    todos map[string]*pb.Todo
}

func (ts *TodoService) Add(t *pb.Todo) error {
    ts.todos[t.Id] = t
    return nil
}

func (ts *TodoService) Get(id string) (*pb.Todo, error) {
    t, ok := ts.todos[id]
    if !ok {
        return nil, fmt.Errorf("no todo")
    }
    return t, nil
}

type Handlers struct {
    service *TodoService
}

func (h *Handlers) LookupTodo(w http.ResponseWriter, r *http.Request) {
    p := mux.Vars(r)

    id, ok := p["id"]
    if !ok {
        w.WriteHeader(http.StatusInternalServerError)
        return
    }

    todo, err := h.service.Get(id)
    if err != nil {
        w.WriteHeader(http.StatusNotFound)
        return
    }

    b, err := json.Marshal(todo)
    if err != nil {
        w.WriteHeader(http.StatusInternalServerError)
        return
    }

    w.WriteHeader(http.StatusOK)
    w.Write(b)
}

func (h *Handlers) CreateTodo(w http.ResponseWriter, r *http.Request) {
    todo := &pb.Todo{}

    decode := json.NewDecoder(r.Body)
    if err := decode.Decode(todo); err != nil {
        w.WriteHeader(http.StatusInternalServerError)
        return
    }

    if err := h.service.Add(todo); err != nil {
        w.WriteHeader(http.StatusInternalServerError)
        return
    }

    w.WriteHeader(http.StatusOK)
}

func main() {
    TodoService := &TodoService{
        todos: make(map[string]*pb.Todo),
    }
    h := &Handlers{service: TodoService}

    mux := mux.NewRouter()
    mux.HandleFunc("/todo/{id}", h.LookupTodo)
    mux.HandleFunc("/todo", h.CreateTodo)

    server := http.Server{
        Addr:    ":8080",
        Handler: mux,
    }

    g, ctx := errgroup.WithContext(context.Background())

    g.Go(func() error {
        log.Println("starting server")
        if err := server.ListenAndServe(); err != nil {
            return nil
        }
        return nil
    })

    g.Go(func() error {
        sigs := make(chan os.Signal, 1)
        // We'll accept graceful shutdowns when quit via SIGINT (Ctrl+C)
        // SIGKILL, SIGQUIT or SIGTERM (Ctrl+/) will not be caught.
        signal.Notify(sigs, os.Interrupt, unix.SIGTERM)
        select {
        case <-ctx.Done():
            log.Println("ctx done, shutting down server")
        case <-sigs:
            log.Println("caught sig, shutting down server")
        }
        ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
        defer cancel()

        if err := server.Shutdown(ctx); err != nil {
            return fmt.Errorf("error in server shutdown: %w", err)
        }
        return nil
    })

    if err := g.Wait(); err != nil {
        log.Fatal(err)
    }
}


The two primary functions we will be focusing on are LookupTodo and CreateTodo .

At first glance, these functions are simple enough. They use the request to either look up a Todo in our system or create the Todo. In both cases, we are using JSON as the expected content-type.

Now that we have a basic flow let's add the ability for the handlers to receive/respond with either JSON or Protobuf.

JSON
 
func (h *Handlers) LookupTodo(w http.ResponseWriter, r *http.Request) {
    p := mux.Vars(r)

    id, ok := p["id"]
    if !ok {
        w.WriteHeader(http.StatusInternalServerError)
        return
    }

    todo, err := h.service.Get(id)
    if err != nil {
        w.WriteHeader(http.StatusNotFound)
        return
    }

    switch r.Header.Get("accept") {
    case "application/json":
        b, err := json.Marshal(todo)
        if err != nil {
            w.WriteHeader(http.StatusInternalServerError)
            return
        }
        w.WriteHeader(http.StatusOK)
        w.Write(b)
    case "application/proto":
        b, err := proto.Marshal(todo)
        if err != nil {
            w.WriteHeader(http.StatusInternalServerError)
            return
        }
        w.WriteHeader(http.StatusOK)
        w.Write(b)
    }
}

func (h *Handlers) CreateTodo(w http.ResponseWriter, r *http.Request) {
    todo := &pb.Todo{}

    switch r.Header.Get("content-type") {
    case "application/json":
        decode := json.NewDecoder(r.Body)
        if err := decode.Decode(todo); err != nil {
            w.WriteHeader(http.StatusInternalServerError)
            return
        }
    case "application/proto":
        b, err := ioutil.ReadAll(r.Body)
        if err != nil {
            w.WriteHeader(http.StatusInternalServerError)
            return
        }
        if err := proto.Unmarshal(b, todo); err != nil {
            w.WriteHeader(http.StatusInternalServerError)
            return
        }
    default:
        w.WriteHeader(http.StatusBadRequest)
        return
    }

    if err := h.service.Add(todo); err != nil {
        w.WriteHeader(http.StatusInternalServerError)
        return
    }

    w.WriteHeader(http.StatusOK)
}


The above example begins to highlight common marshaling patterns. In this case, we have implemented a route that can return two different formats, JSON and Protobuf.

You may be asking, why we would want to do this?

This is a fairly common use case if you want to use a non-human readable format, like Protobuf, to communicate between applications. This often creates a more compact and optimized wire format compared to a more human-readable format such as JSON.

While this is just a small example, it starts to lay out the redundant marshaling logic. Now imagine supporting dozens or hundreds of API routes that all conditionally support unique formats.

That's a lot of marshaling...

marshal

Possible Solution

There are many ways to solve the above redundancy. However, our approach uses Go generics to solve the above problem.

Let's take a look at how we can leverage GLHF to reduce the marshaling logic in our Handlers.

Go
 
func (h *Handlers) LookupTodo(r *glhf.Request[glhf.EmptyBody], w *glhf.Response[pb.Todo]) {
    p := mux.Vars(r.HTTPRequest())

    id, ok := p["id"]
    if !ok {
        w.SetStatus(http.StatusInternalServerError)
        return
    }

    todo, err := h.service.Get(id)
    if err != nil {
        w.SetStatus(http.StatusNotFound)
        return
    }

    w.SetBody(todo)
    w.SetStatus(http.StatusOK)
}

func (h *Handlers) CreateTodo(r *glhf.Request[pb.Todo], w *glhf.Response[glhf.EmptyBody]) {
    if r.Body() == nil {
        w.SetStatus(http.StatusBadRequest)
        return
    }

    if err := h.service.Add(r.Body()); err != nil {
        w.SetStatus(http.StatusInternalServerError)
        return
    }
    w.SetStatus(http.StatusOK)
}


That's it! The above is all it takes to write the same HTTP handler that now supports marshaling both JSON and Protobuff based on the Headers found in the request.

Let's step through GLHF and unpack a few more details.

Generic Handlers

GLHF works by wrapping Go's standard library HTTP handler functions. This ensures that we can work with the standard HTTP package. To do this, we needed to create a few basic types.

First, we start by defining a new Request and Response an object that is specific to GLHF.

Go
 
// Body is the request's body.
type Body any

// A Request represents an HTTP request received by a server
type Request[T Body] struct {
    r    *http.Request
    body *T
}
// Response represents the response from an HTTP request.
type Response[T any] struct {
    w          http.ResponseWriter
    statusCode int
    body       *T
    marshal    MarshalFunc[T]
}


The key to both of these structs is the use of our generic Body type. We are currently not using additional type constraints on the Body but defined it as a type for readability and future type constraints that may be added.

The Request and Response type defines the type of Body during initialization.

The second component of GLHF is a handle function definition that leverages our generic Request and Response structs.

Go
 
type HandleFunc[I Body, O Body] func(*Request[I], *Response[O])


This gives us the ability to write functions that resemble the standard library handler functions.

Lastly, we have defined a set of functions that take a GLHF HandleFunc and return a standard library http.handleFunc.

Here is an example of our Get implementation:

Go
 
func Get[I EmptyBody, O any](fn HandleFunc[I, O], options ...Options) http.HandlerFunc {
    opts := defaultOptions()
    for _, opt := range options {
        opt.Apply(opts)
    }

    return func(w http.ResponseWriter, r *http.Request) {
        var errResp *errorResponse
        if r.Method != http.MethodGet {
            errResp = &errorResponse{
                Code:    http.StatusMethodNotAllowed,
                Message: "invalid method used, expected GET found " + r.Method,
            }
        }

        req := &Request[I]{r: r}
        response := &Response[O]{w: w}

        // call the handler
        fn(req, response)

        if response.body != nil {
            var bodyBytes []byte
            // if there is a custom marshaler, prioritize it
            if response.marshal != nil {
                b, err := response.marshal(*response.body)
                if err != nil {
                    errResp = &errorResponse{
                        Code:    http.StatusInternalServerError,
                        Message: "failed to marshal response with custom marhsaler",
                    }
                }
                bodyBytes = b
            } else {
                // client preferred content-type
                b, err := marshalResponse(r.Header.Get(Accept), response.body)
                if err != nil {
                    // server preferred content-type
                    contentType := response.w.Header().Get(ContentType)
                    if len(contentType) == 0 {
                        contentType = opts.defaultContentType
                    }
                    b, err = marshalResponse(contentType, response.body)
                    if err != nil {
                        errResp = &errorResponse{
                            Code:    http.StatusInternalServerError,
                            Message: "failed to marshal response with content-type: " + contentType,
                        }
                    }
                }
                bodyBytes = b
            }
            // Response failed to marshal
            if errResp != nil {
                w.WriteHeader(errResp.Code)
                if opts.verbose {
                    b, _ := json.Marshal(errResp)
                    w.Write(b)
                }
                return
            }

            // ensure user supplied status code is valid
            if validStatusCode(response.statusCode) {
                w.WriteHeader(response.statusCode)
            }
            if len(bodyBytes) > 0 {
                w.Write(bodyBytes)
            }
            return
        }
    }
}


There is a lot to this function, but at its core, it acts much like a simple middleware. GLHF takes care of the common marshaling flows by attempting to marshal the HTTP request and response into the correct format based on the HTTP Header values for Content-type and Accept.

Ultimately, we can leverage this pattern in a variety of HTTP Routers to simply our HTTP handler logic.

Here is an example of GLHF being used with Gorilla Mux.

Go
 
mux := mux.NewRouter()
    mux.HandleFunc("/todo", glhf.Post(h.CreateTodo))
    mux.HandleFunc("/todo/{id}", glhf.Get(h.LookupTodo))


Closing Thoughts

Hopefully, this gives you a sense of how generics can be used within Go and what problems they can help solve. In short, using generics for HTTP handlers in Golang can provide several benefits, including:

  1. Reusability: Using generics allows you to write code that can work with a variety of types without having to write duplicate code for each type. This makes it easier to reuse code and reduces the amount of boilerplate code that you need to write.
  2. Type Safety: Generics allow you to specify constraints on the types that your code can work with. This can help catch errors at compile time rather than at runtime, improving the overall safety of your code.
  3. Flexibility: Using generics allows you to write code that can work with different types of data, including different data structures and data formats. This can be especially useful when working with data that is coming from external sources.
  4. Performance: Generics can improve the performance of your code by reducing the amount of memory that is used and reducing the number of unnecessary type conversions that are performed.

While generics in Go offer many benefits, there are also some potential drawbacks to consider:

  1. Increased Complexity: Generics can make code more complex and harder to understand, especially for developers unfamiliar with the concept of type parameters.
  2. Increased Maintenance: Generics can make code more abstract, which may require more maintenance and testing to ensure that the code works correctly with all possible types.
  3. Learning Curve: The introduction of generics to Go may require some additional learning for developers who are not familiar with the concept.
  4. Compatibility: The addition of generics to Go may create compatibility issues with existing code that was written before the introduction of generics.

Despite these potential drawbacks, the benefits of generics in Go often outweigh the drawbacks, and many developers are excited about the new possibilities that generics offer.

We have found a lot of value in GLHF and hope others do as well! Head over to our GitHub if you are interested in learning more about GLHF. This blog's source code can be found in the example directory of GLHF.

We have many exciting features and updates planned for GLHF, so stay tuned for upcoming releases!

To stay in the loop on future development, follow us on Twitter or join our Discord! Don't hesitate to make feature requests.

Good luck, and have fun!

JSON Library Go (programming language)

Published at DZone with permission of Simon Cheng. See the original article here.

Opinions expressed by DZone contributors are their own.

Related

  • Adding a Gas Station Map to a React and Go/Gin/Gorm Application
  • What Is API-First?
  • A Camunda Greenhouse: Part 3
  • Go 1.24+ Native FIPS Support for Easier Compliance

Partner Resources

×

Comments
Oops! Something Went Wrong

The likes didn't load as expected. Please refresh the page and try again.

ABOUT US

  • About DZone
  • Support and feedback
  • Community research
  • Sitemap

ADVERTISE

  • Advertise with DZone

CONTRIBUTE ON DZONE

  • Article Submission Guidelines
  • Become a Contributor
  • Core Program
  • Visit the Writers' Zone

LEGAL

  • Terms of Service
  • Privacy Policy

CONTACT US

  • 3343 Perimeter Hill Drive
  • Suite 100
  • Nashville, TN 37211
  • support@dzone.com

Let's be friends:

Likes
There are no likes...yet! 👀
Be the first to like this post!
It looks like you're not logged in.
Sign in to see who liked this post!