Liam Clarke brand
Back to the top
What are Generics
Generic Types
Generic Functions
Generics

Go 1.18 Generics

Programming Style
18 March 2022 4 minute read
Generic Http ServerGeneric ProgrammingGoGolang

With the release of Go 1.18 Generics were added to the language, a major update to the language and with it comes new concepts and syntax. In this article we take a look at what Generics are and how they can be used, refactoring a HTTP server package to make use of the benefits Generics provide.

What are Generics

Generic programming is a style of which a type can be instantiated when being used. This enables many benefits including, stronger types, no type casting and provides reusable solutions.

Generic Types

A Generic is identified by the generic type parameters, these are wrapped around square brackets and are provided after the declared name.

package main

// GenericServer with a Generic type parameter T http.Handler
type GenericServer[T http.Handler] struct {
	Handler T
}

In this given example http.Handler is an interface from the http package, our Generic server type parameter T can be instantiated with any type that conforms to the built-in Handler interface, providing access to all the methods of that type.

Comparing that to a non-generic implementation, we provide the http interface directly to the Handler field.

package main

type Server struct {
	Handler http.Handler
}

The non-generic type conforms to the underlying http interface which contains one method, ServeHTTP(ResponseWriter, *Request) but does not get any other methods for the given type.

Using the Gin framework as an example, we can provide the gin.Engine type and get the strong typing that provides access to all the Gin methods.

package main

import (
	"net/http"

	"github.com/gin-gonic/gin"
)

type GenericServer[T http.Handler] struct {
	Handler T
}

func main() {
	genericServer := GenericServer[*gin.Engine]{}
	genericServer.Handler.GET("/foo", func(ctx *gin.Context) {
		ctx.JSON(http.StatusOK, nil)
	})
    genericServer.Handler.POST("/foo", func(ctx *gin.Context) {
		ctx.JSON(http.StatusOK, nil)
	})
}

Both GET and POST are only available to the Gin web framework, since we provide the gin.Engine as the type parameter, the compiler can interpret the methods by the instantiated type.

In the non-generic implementation, the only method we have access to is the one declared in the interface ServeHTTP(ResponseWriter, *Request).

package main

import (
	"net/http"

	"github.com/gin-gonic/gin"
)

type Server struct {
	Handler http.Handler
}

func main() {
	server := Server{
		Handler: gin.New(),
	}
	// Unresolved reference 'GET'
	server.Handler.GET("/foo", func(ctx *gin.Context) {
		ctx.JSON(http.StatusOK, nil)
	})
}

Trying to access the Gin specific types result in a compilation error, even though the method exists in Gin, we receive an unresolved reference as the gin.Engine type is not instantiated.

Generic Functions

Generic functions are similar to the types in syntax, as well as providing us a way of initialising Generics.

package main

func NewGenericServer[T http.Handler](handler T) *GenericServer[T] {
	return &GenericServer[T]{
		Handler: handler,
	}
}

We define a function with the generic type parameters and use the parameter T as the handler arguments type. This enables us to initialise the GenericServer struct with a given argument of any type that conforms to http.Handler.

The same example as a non-generic function would provide the http.Handler type on the argument.

package main

func NewServer(handler http.Handler) *Server {
	return &Server{
		Handler: handler,
	}
}

With the function accepting a type parameter, we can instantiate the function with the gin.Engine type parameter and make use of its methods.

package main

import (
	"net/http"

	"github.com/gin-gonic/gin"
)

type GenericServer[T http.Handler] struct {
	Handler T
}

func NewGenericServer[T http.Handler](handler T) *GenericServer[T] {
	return &GenericServer[T]{
		Handler: handler,
	}
}

func main() {
	router := gin.New()
	genericServer := NewGenericServer[*gin.Engine](router)
	genericServer.Handler.GET("/foo", func(context *gin.Context) {
		context.JSON(http.StatusOK, nil)
	})
}

In comparison, the same example as a non-generic function would pass the argument types with the arguments but no types can be instantiated, therefore we receive an unresolved reference at compile time.

package main

import (
	"net/http"

	"github.com/gin-gonic/gin"
)

type Server struct {
	Handler http.Handler
}

func NewServer(handler http.Handler) *Server {
	return &Server{
		Handler: handler,
	}
}

func main() {
	router := gin.New()
	server := NewServer(router)
	// Unresolved reference 'GET'
	server.Handler.GET("/foo", func(context *gin.Context) {
		context.JSON(http.StatusOK, nil)
	})
}

Generics

Generics are a powerful feature that provide the ability to instantiate types at compile time, enabling the reuse of types and functions that can be used with improved stronger typing without the need for casting.